fix: serialize LAMMPS HDF5 trajectories - #381
Conversation
|
Warning Review limit reachedNext included review available in 59 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Return trajectory text from RunLmpHDF5 so its output matches the declared HDF5 dataset contract. Closes deepmodeling#355 Coding-Agent: Codex Codex-Version: codex-cli 0.149.1 Model: gpt-5.6-sol Reasoning-Effort: xhigh
bac4a98 to
22146e4
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #381 +/- ##
==========================================
+ Coverage 84.43% 84.52% +0.09%
==========================================
Files 104 104
Lines 6110 6114 +4
==========================================
+ Hits 5159 5168 +9
+ Misses 951 946 -5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Retracted. This review was produced without running the mandated /code-review fan-out (the loop skill's section 2); the substitute process used instead has since been shown to miss findings and, in one case, to state a verified-sounding falsehood. Re-reviewing properly.
wanghan-iapcm
left a comment
There was a problem hiding this comment.
The diagnosis in #355 is right and the hook is the right place to fix it, but the fix does not change what lands in the HDF5 file. I verified this by running dflow's real serialization path rather than reading it.
The fix is a no-op in every reachable configuration
dflow/python/utils.py handle_output_artifact does, for an Artifact(HDF5Datasets) output:
if isinstance(slices, list):
items = [(str(s), v) for s, v in zip(slices, value)]
else:
items = flatten(value).items()and dflow.utils.flatten only recurses into list / dict. A scalar of any other type never reaches the create_dataset loop. Round-tripping the real function:
scalar Path slices=0 (int) -> keys = [] empty h5 <- master, i.e. the bug in #355
scalar str slices=0 (int) -> keys = [] empty h5 <- this PR
scalar ndarray slices=0 (int) -> keys = [] empty h5 <- get_model_devi
[str] slices=0 (int) -> keys = ['0'] b'trajectory data'
[Path] slices=0 (int) -> keys = ['0'] attrs: type=file, path=..., dtype=utf-8
scalar any slices=[0] -> AssertionError
[str] / [Path] slices=[0] -> keys = ['0']
group_size and pool_size both default to None (dpgen2/utils/step_config.py), so Slices("int('{{item}}')", ...) in dpgen2/superop/prep_run_lmp.py renders slices as a plain int and the flatten branch is taken. And in the grouped configuration the PR is equally a no-op, because a list of Path already serialized correctly before it — dflow's Path branch does the read_text itself. So there is no configuration in which this change alters the resulting .h5.
Downstream, TrajRenderLammps.get_confs computes ntraj = len(trajs) == 0 and loops zero times, so use_hdf5: true selects zero configurations every iteration, silently.
The shape that works is already in the tree
RunRelaxHDF5 is wired with the identical Slices("int('{{item}}')") in dpgen2/superop/prep_run_diffcsp.py and works, because RunRelax.execute builds trajs = [] / model_devis = [] and appends. That list-vs-scalar difference is the whole thing.
For the record on how long this has been broken
08d8d6e (#267, 2024-10-21) declared both traj and model_devi as Artifact(HDF5Datasets), added the get_model_devi hook, and added the use_hdf5 switch — all in one commit — while leaving "traj": work_dir / lmp_traj_name on the line above unconverted. Not drift; an omission from day one. grep -rn "HDF5\|hdf5" tests/ returns nothing, and use_hdf5 appears in no example, test or doc, so neither HDF5 subclass has ever been exercised.
Details inline. The three code findings share one root cause and one change fixes them together.
|
|
||
| def get_traj(self, traj_file): | ||
| """Return trajectory text for serialization into an HDF5 dataset.""" | ||
| return traj_file.read_text() |
There was a problem hiding this comment.
This is the line to change. read_text() returns a str, which is exactly as much a scalar as the Path it replaced, so flatten drops it and the dataset loop never runs. Confirmed against the installed dflow: flatten('trajectory data') is {}, and handle_output_artifact with Artifact(HDF5Datasets) and an int slices produces an .h5 with keys = [].
Suggest returning a list containing the Path, not the text:
def get_traj(self, traj_file):
return [traj_file]The list is what flatten needs. Keeping it a Path also routes through dflow's own Path branch, which read_text() bypasses:
if v.is_file():
try:
data = v.read_text(encoding="utf-8"); dtype = "utf-8"
except Exception:
data = np.void(v.read_bytes()); dtype = "binary"
d = f.create_dataset(s, data=data)
d.attrs["type"] = "file"; d.attrs["path"] = str(v); d.attrs["dtype"] = dtypeSo you get the is_file() guard, a binary fallback for a non-UTF-8 dump, and the type/path attrs for free. As written, a missing dump raises a bare FileNotFoundError out of execute() and a non-UTF-8 dump raises UnicodeDecodeError — I reproduced both. Those are unlikely in practice since dpgen2 generates the dump directive itself, but there is no reason to give up the guards. [Path] still arrives at the consumer as decoded text, because HDF5Dataset.get_data() decodes on dtype == "utf-8" in both branches.
The docstring on this method also needs updating: "Return trajectory text for serialization into an HDF5 dataset" asserts that returning the text is what causes serialization, and that is what is not true. Note too that RunLmpHDF5 does not override execute, so it inherits RunLmp.execute's Returns section, which still documents traj and model_devi as Artifact(Path).
| @@ -412,3 +416,7 @@ def get_output_sign(cls): | |||
|
|
|||
| def get_model_devi(self, model_devi_file): | |||
| return np.loadtxt(model_devi_file) | |||
There was a problem hiding this comment.
This has the identical defect and needs to move in the same change, not a follow-up. np.loadtxt returns an ndarray, which is not a list or dict, so flatten drops it too — keys = [].
The reason it cannot wait: if only traj becomes a list, the two artifacts arrive downstream with different lengths, and dpgen2/exploration/selector/conf_selector_frame.py:88-89 is
ntraj = len(trajs)
assert ntraj == len(model_devis)I simulated all three states through the real handle_output_artifact + handle_input_artifact round trip with two tasks:
both scalar (current head) trajs=0 model_devis=0 assert passes vacuously, silent data loss
traj list, model_devi scalar trajs=2 model_devis=0 AssertionError mid-workflow
both lists trajs=2 model_devis=2 correct
So fixing traj alone is worse than fixing neither. return [np.loadtxt(model_devi_file)] alongside the traj change.
| ) | ||
| ) | ||
|
|
||
| self.assertEqual(out["traj"], "trajectory data") |
There was a problem hiding this comment.
This test is not vacuous — I checked, reverting get_traj to the bare Path makes it fail with AssertionError: PosixPath('task_000/traj.dump') != 'trajectory data'. But it asserts one layer above the defect, so it is green while the artifact it exists to protect is empty.
out["traj"] is the OP's in-memory return value. The failure lives in dflow's serialization of that value, which this test never invokes. I fed the exact value the test asserts on into the real handle_output_artifact and got an .h5 with zero datasets.
Issue #355 asked for "a test for RunLmpHDF5.execute output types". The letter is satisfied; the contract is not. A round-trip assertion is what would have caught this and what would stop it regressing:
from dflow.python.utils import handle_output_artifact
handle_output_artifact("traj", out["traj"], Artifact(HDF5Datasets), slices=0, data_root=tmp)
# then open the produced .h5 and assert its key set is non-emptyWorth doing for model_devi in the same test. Minor, while you are here: this lives in TestRunLmp but exercises RunLmpHDF5; a separate TestRunLmpHDF5 class would read better.
Summary
Tests
PYTHONPATH=tests python -m unittest -v tests.op.test_run_lmp.TestRunLmp.test_hdf5_outputs_dataset_values tests.op.test_run_lmp.TestRunLmp.test_successisort --check-only dpgen2/op/run_lmp.py tests/op/test_run_lmp.pygit diff --checkCloses #355
Coding agent: Codex
Codex version: codex-cli 0.149.0
Model: gpt-5.6-sol
Reasoning effort: xhigh